/*
 * License GNU LGPL
 * Copyright (C) 2012 Amrullah <amrullah@panemu.com>.
 */
package com.abc.cheque.control.skin;


import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.event.EventTarget;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.PopupControl;
import javafx.scene.control.Skin;
import javafx.scene.control.Skinnable;
import javafx.scene.control.TextField;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.InputEvent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.util.Callback;

import org.apache.commons.beanutils.PropertyUtils;

import com.abc.cheque.common.TableCriteria;
import com.abc.cheque.control.LookupField;
import com.abc.cheque.control.behavior.LookupFieldBehavior;
import com.abc.cheque.ui.form.BaseControl;
import com.sun.javafx.scene.control.FocusableTextField;
import com.sun.javafx.scene.control.skin.SkinBase;

/**
 *
 * @author Amrullah <amrullah@panemu.com>
 */
public class LookupFieldSkin<T> extends SkinBase<LookupField<T>, LookupFieldBehavior<T>> {

    private static final String PROP_SHOWING_SUGGESTION = "SHOWING_SUGGESTION";
    private static final String PROP_SHOWING_LOOKUP_WINDOW = "SHOWING_LOOKUP_WINDOW";
    private static final String PROP_RESETTING_DISPLAY_TEXT = "RESETTING_DISPLAY_TEXT";
    private TextField textField;
    private Button button;
    private PopupControl popup;
    private boolean detectTextChanged = true;
    private Timer waitTimer;
    private LoaderTimerTask loaderTimerTask;
    private LookupField<T> lookupField;
    /**
     * flag to
     */
    public boolean needValidation = true;

    public LookupFieldSkin(LookupField<T> control) {
        super(control, new LookupFieldBehavior<>(control));
        this.lookupField = control;
        // move focus in to the textfield
        lookupField.focusedProperty().addListener(new ChangeListener<Boolean>() {
            @Override
            public void changed(ObservableValue<? extends Boolean> ov, Boolean t, Boolean t1) {
                if (!t1) {
                    validate();
                }
                updateFakeFocus(lookupField.isFocused());
            }
        });
        initialize();

        lookupField.addEventFilter(InputEvent.ANY, new EventHandler<InputEvent>() {
            @Override
            public void handle(InputEvent t) {
                if (textField == null) {
                    return;
                }

                // When the user hits the enter or F4 keys, we respond before 
                // ever giving the event to the TextField.
                if (t instanceof KeyEvent) {
                    KeyEvent ke = (KeyEvent) t;

                    if ((ke.getCode() == KeyCode.F10 || ke.getCode() == KeyCode.ESCAPE || ke.getCode() == KeyCode.ENTER)
                            && !ke.isControlDown()) {
//                        if (ke.getCode() == KeyCode.ENTER && !ke.isControlDown()) {
//                            showLookupDialog(false);
//                        }

                        // RT-23275: The TextField fires F10 and ESCAPE key events
                        // up to the parent, which are then fired back at the 
                        // TextField, and this ends up in an infinite loop until
                        // the stack overflows. So, here we consume these two
                        // events and stop them from going any further.
                        t.consume();
                        return;
                    }
                }

                textField.fireEvent(t);
            }
        });
        requestLayout();

        registerChangeListener(control.showingSuggestionProperty(), PROP_SHOWING_SUGGESTION);
        registerChangeListener(control.showingLookupDialogProperty(), PROP_SHOWING_LOOKUP_WINDOW);
        registerChangeListener(control.resettingDisplayTextProperty(), PROP_RESETTING_DISPLAY_TEXT);
    }

    @Override
    protected void handleControlPropertyChanged(String string) {
        super.handleControlPropertyChanged(string);
        if (PROP_SHOWING_SUGGESTION.equals(string)) {
            if (lookupField.isShowingSuggestion()) {
                showSuggestion();
            } else {
                getPopup().hide();
            }
        } else if (PROP_SHOWING_LOOKUP_WINDOW.equals(string)) {
            if (lookupField.isShowingLookupDialog()) {
                showLookupDialog(false);
            }
        } else if (PROP_RESETTING_DISPLAY_TEXT.equals(string)) {
            if (lookupField.isResettingDisplayText()) {
                updateTextField();
            }
        }
    }

    private PopupControl getPopup() {
        if (popup == null) {
            createPopup();
        }
        return popup;
    }

    private double getListViewPrefHeight() {
        double ph;
        double ch = listView.getItems().size() * 25;
        ph = Math.min(ch, 200);

        return ph;
    }

    private void createPopup() {
        popup = new PopupControl() {
            {
                setSkin(new Skin() {
                    @Override
                    public Skinnable getSkinnable() {
                        return LookupFieldSkin.this.lookupField;
                    }

                    @Override
                    public Node getNode() {
                        return listView;
                    }

                    @Override
                    public void dispose() {
                    }
                });
            }
        };
        popup.setAutoHide(true);
        popup.setAutoFix(true);
        popup.setHideOnEscape(true);
        popup.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent t) {
                popup.hide();
            }
        });

        listView.setCellFactory(new Callback() {
            @Override
            public Object call(Object p) {
                return new PropertyListCell(lookupField.getPropertyName());
            }
        });

        /**
         * Taken from
         * {@link com.sun.javafx.scene.control.skin.ComboBoxListViewSkin}
         */
        listView.addEventFilter(MouseEvent.MOUSE_RELEASED, new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent t) {
                // RT-18672: Without checking if the user is clicking in the 
                // scrollbar area of the ListView, the comboBox will hide. Therefore,
                // we add the check below to prevent this from happening.
                EventTarget target = t.getTarget();
                if (target instanceof Parent) {
                    List<String> s = ((Parent) target).getStyleClass();
                    if (s.contains("thumb")
                            || s.contains("track")
                            || s.contains("decrement-arrow")
                            || s.contains("increment-arrow")) {
                        return;
                    }
                }
                needValidation = false;
                lookupField.setValue(listView.getSelectionModel().getSelectedItem());
                popup.hide();
            }
        });

        listView.setOnKeyPressed(new EventHandler<KeyEvent>() {
            @Override
            public void handle(KeyEvent t) {
                if (t.getCode() == KeyCode.ENTER) {
                    needValidation = false;
                    lookupField.setValue(listView.getSelectionModel().getSelectedItem());
                    popup.hide();
                } else if (t.getCode() == KeyCode.ESCAPE) {
                    popup.hide();
                }
            }
        });

    }

    private void initialize() {
        textField = new FocusableTextField();
        textField.setFocusTraversable(true);
        textField.focusedProperty().addListener(new ChangeListener<Boolean>() {
            @Override
            public void changed(ObservableValue<? extends Boolean> ov, Boolean t, Boolean hasFocus) {
                if (hasFocus) {
                    // Using Platform.runLater here, as without it it seems we
                    // enter into some form of race condition where we are 
                    // wanting to set focus on the comboBox whilst the textField
                    // is still notifying of its focus gain.
                    // This issue was identified in RT-21088.
                    Platform.runLater(new Runnable() {
                        @Override
                        public void run() {
                            lookupField.requestFocus();
                        }
                    });
                }
                textField.selectEnd();
//                updateFakeFocus(hasFocus);

//                if (!hasFocus) {
//                    validate();
//                }
            }
        });


        button = new Button();
        button.setFocusTraversable(false);
        button.setGraphic(new ImageView(new Image(BaseControl.class.getResourceAsStream("/images/lookup.png"))));
        StackPane.setAlignment(textField, Pos.CENTER_LEFT);
        StackPane.setAlignment(button, Pos.CENTER_RIGHT);
        this.getChildren().addAll(textField, button);
        button.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent t) {
                if (!lookupField.isFocused()) {
                    /**
                     * Need to make this control become focused. Otherwise
                     * changing value in LookupColumn while the LookuField cell editor
                     * is not focused before, won't trigger commitEdit()
                     */
                    lookupField.requestFocus();
                }
                getSkinnable().showLookupDialog();
            }
        });
        updateTextField();
        lookupField.valueProperty().addListener(new ChangeListener() {
            @Override
            public void changed(ObservableValue ov, Object t, Object t1) {
                updateTextField();
            }
        });

        lookupField.markInvalidProperty().addListener(new ChangeListener<Boolean>() {
            @Override
            public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
                if (oldValue && !newValue && needValidation) {
                    validate();
                }
            }
        });

        textField.textProperty().addListener(new InvalidationListener() {
            @Override
            public void invalidated(Observable o) {
                if (detectTextChanged) {
                    if (waitTimer != null) {
                        loaderTimerTask.setObsolete(true);
                        waitTimer.cancel();
                        waitTimer.purge();
                    }

                    if (textField.getText() == null || textField.getText().trim().isEmpty()) {
                        lookupField.setValue(null);
                        return;
                    }
                    lookupField.markInvalidProperty().set(true);
                    needValidation = true;


                    if (lookupField.getShowSuggestionWaitTime() >= 0) {
                        waitTimer = new Timer("lookupTimer");
                        loaderTimerTask = new LoaderTimerTask(waitTimer);
                        waitTimer.schedule(loaderTimerTask, lookupField.getShowSuggestionWaitTime());
                    }
                }
            }
        });

    }

    private void updateFakeFocus(boolean b) {
        ((FocusableTextField) textField).setFakeFocus(b);
    }

    private void updateTextField() {
        detectTextChanged = false;
        needValidation = false;
        if (lookupField.getValue() == null) {
            textField.setText("");
            lookupField.markInvalidProperty().set(false);
            detectTextChanged = true;
            return;
        }
        try {
            Object value = PropertyUtils.getSimpleProperty(lookupField.getValue(), lookupField.getPropertyName());
            textField.setText(value.toString());
            lookupField.markInvalidProperty().set(false);
            detectTextChanged = true;
        } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) {
            throw new RuntimeException(ex);
        }
    }

    private Point2D getPrefPopupPosition() {
        return com.sun.javafx.Utils.pointRelativeTo(lookupField, listView, HPos.CENTER, VPos.BOTTOM, -7, 0, false);
    }

    private void positionAndShowPopup() {
        if (getPopup().getSkin() == null) {
            getScene().getRoot().impl_processCSS(true);
        }

        Point2D p = getPrefPopupPosition();

        /**
         * In LookupColumn, sometimes the lookupfield disappears due to commit
         * editing before the popup appears. In this case,
         * lookupField.getScene() will be null.
         */
        Scene scene = lookupField.getScene();
        if (scene != null) {
            getPopup().show(scene.getWindow(), p.getX(), p.getY());
        }
    }

    @Override
    protected void layoutChildren() {
        double obw = button.prefWidth(-1);

        double displayWidth = getSkinnable().getWidth()
                - (getInsets().getLeft() + getInsets().getRight() + obw);

        Insets padding = getInsets();
        double x = padding.getLeft();
        double y = padding.getTop();
        final double w = getSkinnable().getWidth() - (padding.getLeft() + padding.getRight());
        final double h = getSkinnable().getHeight() - (padding.getTop() + padding.getBottom());
        textField.resizeRelocate(x, y, w, h);
        button.resizeRelocate(x + displayWidth, y, obw, h);
    }

    static class PropertyListCell<T> extends ListCell<T> {

        private String propertyName;

        public PropertyListCell(String propertyName) {
            this.propertyName = propertyName;
        }

        @Override
        protected void updateItem(T t, boolean bln) {
            super.updateItem(t, bln);
            if (t != null) {
                try {
                    Object value = PropertyUtils.getSimpleProperty(t, propertyName);
                    if (value != null) {
                        setText(value.toString());
                    }
                } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) {
                    throw new RuntimeException(ex);
                }
            }
        }
    }

    private void validate() {
        if (needValidation && !textField.getText().isEmpty()) {
            loaderTimerTask.setObsolete(true);
            List<T> data = lookupField.getController().loadDataForPopup(lookupField.getPropertyName(), textField.getText(), TableCriteria.Operator.eq);
            if (data.size() == 1) {
                lookupField.setValue(data.get(0));
            } else {
                showLookupDialog(true);
            }
        }
    }

    private void showLookupDialog(boolean filterIt) {
        needValidation = false;
        T newValue;
        if (filterIt) {
            newValue = lookupField.getController().show(lookupField.getScene().getWindow(), lookupField.getValue(), lookupField.getPropertyName(), textField.getText());
        } else {
            newValue = lookupField.getController().show(lookupField.getScene().getWindow(), lookupField.getValue(), lookupField.getPropertyName());
        }
        if (newValue == lookupField.getValue()) {
            updateTextField();
        } else {
            lookupField.setValue(newValue);
        }
    }

    private void showSuggestion() {
        List<T> data = lookupField.getController().loadDataForPopup(lookupField.getPropertyName(), textField.getText());
        listView.getItems().clear();
        listView.getItems().addAll(data);
        positionAndShowPopup();
    }

    private class LoaderTimerTask extends TimerTask {

        private boolean obsolete = false;
        private Timer timer;

        public LoaderTimerTask(Timer timer) {
            this.timer = timer;
        }

        public void setObsolete(boolean obsolete) {
            this.obsolete = obsolete;
        }

        @Override
        public void run() {
            if (!obsolete) {
                final List<T> data = lookupField.getController().loadDataForPopup(lookupField.getPropertyName(), textField.getText());
                Platform.runLater(new Runnable() {
                    @Override
                    public void run() {
                        if (!obsolete) {
                            listView.getItems().clear();
                            if (!data.isEmpty()) {
                                listView.getItems().addAll(data);
                                positionAndShowPopup();
                            }
                        }
                    }
                });
            }
            timer.cancel();
            timer.purge();
        }
    }
    private ListView<T> listView = new ListView<T>() {
        @Override
        protected double computeMinHeight(double width) {
            return 30;
        }

        @Override
        protected double computePrefHeight(double width) {

            return getListViewPrefHeight();
        }
    };
}
